﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using Elderos.Controls;
using Elderos.Utils;
using Microsoft.Samples.CustomControls;
using TreeBuilder.Settings;

namespace TreeBuilder
{
    /// <summary>
    /// Interaction logic for NodeFrame.xaml
    /// </summary>
    public partial class NodeFrame : UserControl, INode, INameScope, IMenuChangeableObject
    {
        public NodeFrame()
        {
            NeedsCanvasAlignment = false;
            InitializeComponent();
            Initialize(
                FrameSettings.Default.Brush,
                FrameSettings.Default.Thickness,
                FrameSettings.Default.DashStyle
                );
        }

        private void Initialize(Brush brush, double thickness, DoubleCollection dashStyle)
        {
            MainPath.Stroke = brush;
            DesiredThickness = thickness;
            MainPath.StrokeDashArray = dashStyle ?? new DoubleCollection();

            NameScope.SetNameScope(MainPath.ContextMenu, this);
        }

        public event Action<object, EventArgs> Delete;

        protected virtual void OnDelete(object sender)
        {
            Action<object, EventArgs> handler = Delete;
            if (handler != null) handler(sender, EventArgs.Empty);
        }

        #region INameScope Members

        readonly Dictionary<string, object> _items = new Dictionary<string, object>();

        object INameScope.FindName(string name)
        {
            return _items[name];
        }

        void INameScope.RegisterName(string name, object scopedElement)
        {
            _items.Add(name, scopedElement);
        }

        void INameScope.UnregisterName(string name)
        {
            _items.Remove(name);
        }

        #endregion

        #region Implementation of IPrintable

        public bool IsBeingPrinted
        {
            get { return (bool)GetValue(IsBeingPrintedProperty); }
            set { SetValue(IsBeingPrintedProperty, value); }
        }

        // Using a DependencyProperty as the backing store for IsBeingPrinted.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsBeingPrintedProperty =
            DependencyProperty.Register("IsBeingPrinted", typeof(bool), typeof(NodeFrame), new UIPropertyMetadata(false));

        #endregion

        #region Implementation of INode

        public event Action<object, DragInitEventArgs> DragInit;

        protected virtual void OnDragInit(object sender, DragInitEventArgs args)
        {
            Action<object, DragInitEventArgs> handler = DragInit;
            if (handler != null) handler(sender, args);
        }

        #region Implementation of IBindable
        public bool IsBeingLinked
        {
            get { return (bool)GetValue(IsBeingLinkedProperty); }
            set
            {
                SetValue(IsBeingLinkedProperty, value);
            }
        }

        public static readonly DependencyProperty IsBeingLinkedProperty =
            DependencyProperty.Register("IsBeingLinked", typeof(bool), typeof(NodeFrame), new UIPropertyMetadata(false));

        public event Action<object, InitBindingEventArgs> InitBinding;

        protected virtual void OnInitBinding(object sender, InitBindingEventArgs e)
        {
            Action<object, InitBindingEventArgs> handler = InitBinding;
            if (handler != null) handler(sender, e);
        }

        public event Action<object, CompleteBindingEventArgs> CompleteBinding;

        protected virtual void OnCompleteBinding(object sender, CompleteBindingEventArgs e)
        {
            Action<object, CompleteBindingEventArgs> handler = CompleteBinding;
            if (handler != null) handler(sender, e);
        }

        private List<IBinder> _links = new List<IBinder>();

        public List<IBinder> Links
        {
            get { return _links; }
        }

        #endregion Implementation of IBindable

        public Point? Offset { get; set; }

        private Guid _guid = Guid.NewGuid();
        public Guid Guid
        {
            get { return _guid; }
            set { _guid = value; }
        }

        public event Action<object, SelectionEventArgs> Selected;

        protected virtual void OnSelected(object sender, SelectionEventArgs e)
        {
            Action<object, SelectionEventArgs> handler = Selected;
            if (handler != null) handler(sender, e);
        }

        public bool Intersects(Rect selectionRect)
        {
            return this.Location.IntersectsWith(selectionRect);
        }

        public bool IsSelected
        {
            get { return (bool)GetValue(IsSelectedProperty); }
            set
            {
                SetValue(IsSelectedProperty, value);
                InvalidateAdorners();
            }
        }

        public static readonly DependencyProperty IsSelectedProperty =
            DependencyProperty.Register("IsSelected", typeof(bool), typeof(NodeFrame), new PropertyMetadata(false));

        private ResizeAdorner _adorner;

        private void InvalidateAdorners()
        {
            var adornerLayer = AdornerLayer.GetAdornerLayer(MainPath);
            if (adornerLayer == null) return;

            if (_adorner == null) return;

            var adorners = adornerLayer.GetAdorners(MainPath);

            if (IsSelected)
            {
                if (adorners == null || !adorners.Contains(_adorner))
                    adornerLayer.Add(_adorner);
            }
            else
                adornerLayer.Remove(_adorner);
        }

        public event Action<UndoableAction> MoveAndResizeComplete;

        protected virtual void OnMoveAndResizeComplete(UndoableAction obj)
        {
            Action<UndoableAction> handler = MoveAndResizeComplete;
            if (handler != null) handler(obj);
        }

        public void InitializeSelectionAdorners()
        {
            _adorner = new ResizeAdorner(MainPath, this, ButtonPanelHeight, MinSize);
            _adorner.MoveAndResizeComplete += OnMoveAndResizeComplete;
        }

        public Size MinSize
        {
            get
            {
                double offset = MainPath.StrokeThickness / 2;
                double frameSize = ApplicationSettings.Default.MinimalFrameSize + offset;
                return new Size(frameSize, frameSize);
            }
        }

        private void MainPath_OnLoaded(object sender, RoutedEventArgs e)
        {
            InitializeSelectionAdorners();
        }

        public bool IsCreated
        {
            get { return (bool)GetValue(IsCreatedProperty); }
            set { SetValue(IsCreatedProperty, value); }
        }

        public static readonly DependencyProperty IsCreatedProperty =
            DependencyProperty.Register("IsCreated", typeof(bool), typeof(NodeFrame), new UIPropertyMetadata(false));



        public double DesiredThickness
        {
            get { return (double)GetValue(DesiredThicknessProperty); }
            set { SetValue(DesiredThicknessProperty, value); }
        }

        // Using a DependencyProperty as the backing store for DesiredThickness.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DesiredThicknessProperty =
            DependencyProperty.Register("DesiredThickness", typeof(double), typeof(NodeFrame), new UIPropertyMetadata(FrameSettings.Default.Thickness));



        public Size Size
        {
            get { return (Size)GetValue(SizeProperty); }
            set
            {
                SetValue(SizeProperty, value);
                OnSizeChanged();
            }
        }

        // Using a DependencyProperty as the backing store for Size.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SizeProperty =
            DependencyProperty.Register("Size", typeof(Size), typeof(NodeFrame), new UIPropertyMetadata(default(Size)));

        protected void OnSizeChanged()
        {
            NormalizePathThickness();
            RedrawFrame();
        }

        private void NormalizePathThickness()
        {
            MainPath.StrokeThickness = Math.Min((Math.Min(Size.Width, Size.Height) / 4), DesiredThickness);
            MainPath.StrokeThickness = Math.Max(MainPath.StrokeThickness, ApplicationSettings.Default.MinBinderThickness);
        }

        private void RedrawFrame()
        {
            double offset = MainPath.StrokeThickness/2;
            double frameSize = ApplicationSettings.Default.MinimalFrameSize + offset;

            double width = this.Size.Width - offset;
            double height = this.Size.Height - offset;

            frameSize = Math.Min(frameSize, width/2);
            frameSize = Math.Min(frameSize, height/2);

            PathFigure.StartPoint = new Point(frameSize, offset);
            UpperLine.Point = new Point(width - frameSize, offset);
            UpperRightBezier.Point1 = new Point(width, offset);
            UpperRightBezier.Point2 = new Point(width, offset);
            UpperRightBezier.Point3 = new Point(width, frameSize);
            RightLine.Point = new Point(width, height - frameSize);
            LowerRightBezier.Point1 = new Point(width, height);
            LowerRightBezier.Point2 = new Point(width, height);
            LowerRightBezier.Point3 = new Point(width - frameSize, height);
            BottomLine.Point = new Point(frameSize, height);
            LowerLeftBezier.Point1 = new Point(offset, height);
            LowerLeftBezier.Point2 = new Point(offset, height);
            LowerLeftBezier.Point3 = new Point(offset, height - frameSize);
            LeftLine.Point = new Point(offset, frameSize);
            UpperLeftBezier.Point1 = new Point(offset, offset);
            UpperLeftBezier.Point2 = new Point(offset, offset);
            UpperLeftBezier.Point3 = new Point(frameSize, offset);
        }


        public Point TopCenter
        {
            get { return (Point)GetValue(TopCenterProperty); }
            set 
            {
                double pathWidth = MainPath.ActualWidth;
                double width = this.ActualWidth;

                double horizontalOffset = (pathWidth - width) / 2;

                double x = Math.Max(value.X, 0 + horizontalOffset + width/2);
                double y = Math.Max(value.Y, 0 - ButtonPanel.ActualHeight);

                SetValue(TopCenterProperty, new Point(x, y));
                OnTopCenterChanged();
            }
        }

        public Rect Location 
        {
            get
            {
                double x = Canvas.GetLeft(this);
                double y = Canvas.GetTop(this) + ButtonPanel.ActualHeight;
                double width = MainPath.ActualWidth;
                double height = MainPath.ActualHeight;
                return new Rect(x, y, width, height);
            } 
        }

        public virtual void OnTopCenterChanged()
        {
            Canvas.SetLeft(this, TopCenter.X - ActualWidth/2);
            Canvas.SetTop(this, Math.Max(TopCenter.Y, 0) - ButtonPanel.ActualHeight);

            _links.ForEach(x => x.RefreshPoints());
        }

        public double ButtonPanelHeight
        {
            get { return ButtonPanel.ActualHeight; }
        }

        public bool NeedsCanvasAlignment { get; set; }

        // Using a DependencyProperty as the backing store for TopCenter.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TopCenterProperty =
            DependencyProperty.Register("TopCenter", typeof(Point), typeof(NodeFrame), new UIPropertyMetadata(default(Point)));

        public Point Center
        {
            get
            {
                double x = TopCenter.X;
                double y = Canvas.GetTop(this) + ButtonPanel.ActualHeight;
                return new Point(x, y);
            }
        }

        public Point BottomCenter
        {
            get
            {
                double x = TopCenter.X;
                double y = Canvas.GetTop(this) + this.ActualHeight;
                return new Point(x, y);
            }
        }

        public Point RightCenter
        {
            get
            {
                double x = Canvas.GetLeft(this) + Math.Max(MainPath.ActualWidth, ButtonPanel.ActualWidth);
                if (ButtonPanel.ActualWidth > MainPath.ActualWidth)
                    x -= (ButtonPanel.ActualWidth - MainPath.ActualWidth) / 2;
                double y = Canvas.GetTop(this) + ButtonPanel.ActualHeight + MainPath.ActualHeight / 2;
                return new Point(x, y);
            }
        }

        public Point LeftCenter
        {
            get
            {
                double x = Canvas.GetLeft(this);
                if (ButtonPanel.ActualWidth > MainPath.ActualWidth)
                    x += (ButtonPanel.ActualWidth - MainPath.ActualWidth) / 2;
                double y = Canvas.GetTop(this) + ButtonPanel.ActualHeight + MainPath.ActualHeight / 2;
                return new Point(x, y);
            }
        }

        #endregion

        #region Implementation of IXmlSerializable

        /// <summary>
        /// This method is reserved and should not be used. When implementing the IXmlSerializable interface, you should return null (Nothing in Visual Basic) from this method, and instead, if specifying a custom schema is required, apply the <see cref="T:System.Xml.Serialization.XmlSchemaProviderAttribute"/> to the class.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Xml.Schema.XmlSchema"/> that describes the XML representation of the object that is produced by the <see cref="M:System.Xml.Serialization.IXmlSerializable.WriteXml(System.Xml.XmlWriter)"/> method and consumed by the <see cref="M:System.Xml.Serialization.IXmlSerializable.ReadXml(System.Xml.XmlReader)"/> method.
        /// </returns>
        public XmlSchema GetSchema()
        {
            return null;
        }

        /// <summary>
        /// Generates an object from its XML representation.
        /// </summary>
        /// <param name="reader">The <see cref="T:System.Xml.XmlReader"/> stream from which the object is deserialized. </param>
        public void ReadXml(XmlReader reader)
        {
            XDocument xdoc = XDocument.Load(reader);
            var root = xdoc.Root;

            Brush brush = (SolidColorBrush)XamlReader.Parse(root.Element("Brush").Value);
            double thickness = double.Parse(root.Element("Thickness").Value.Replace(",", "."), CultureInfo.InvariantCulture);
            DoubleCollection dashStyle = DoubleCollection.Parse(root.Element("DashStyle").Value.Replace(",", "."));

            Initialize(brush, thickness, dashStyle);

            Size = (Size)XamlReader.Parse(root.Element("Size").Value);
            TopCenter = XmlSerializationUtility.Deserialize<Point>(root.Element("TopCenter").Value);

            Guid = XmlSerializationUtility.Deserialize<Guid>(root.Element("Guid").Value);
            IsCreated = true;
        }

        /// <summary>
        /// Converts an object into its XML representation.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Xml.XmlWriter"/> stream to which the object is serialized. </param>
        public void WriteXml(XmlWriter writer)
        {
            writer.WriteElementString("Brush", XamlWriter.Save(MainPath.Stroke));
            writer.WriteElementString("Thickness", DesiredThickness.ToString());
            writer.WriteElementString("DashStyle", MainPath.StrokeDashArray == null ? "" : MainPath.StrokeDashArray.ToString());
            writer.WriteElementString("Size", XamlWriter.Save(Size));
            writer.WriteElementString("TopCenter", XmlSerializationUtility.Serialize(TopCenter));
            writer.WriteElementString("Guid", XmlSerializationUtility.Serialize(Guid));
        }

        #endregion

        private void ArrowButton_Click(object sender, RoutedEventArgs e)
        {
            OnInitBinding(sender, new InitBindingEventArgs(() => new ArrowLink()));
        }

        private void DeleteButton_Click(object sender, RoutedEventArgs e)
        {
            OnDelete(sender);
        }

        private void LineButton_Click(object sender, RoutedEventArgs e)
        {
            OnInitBinding(sender, new InitBindingEventArgs(() => new LineLink()));
        }

        private void TriangleButton_Click(object sender, RoutedEventArgs e)
        {
            OnInitBinding(sender, new InitBindingEventArgs(() => new TriangleLink()));
        }

        private void Rectangle_GotFocus(object sender, RoutedEventArgs e)
        {
            ButtonPanel.Visibility = Visibility.Visible;

            //if (!IsSelected)
            //	OnSelected(sender, new SelectionEventArgs(this));
        }

        private void Rectangle_LostFocus(object sender, RoutedEventArgs e)
        {
            ButtonPanel.Visibility = Visibility.Hidden;
        }

        private double _realThickness;
        public double RealThickness
        {
            get { return _realThickness; }
            private set
            {
                _realThickness = value;
            }
        }

        private bool _thicknessChanged;

        protected override void OnMouseEnter(MouseEventArgs e)
        {
            base.OnMouseEnter(e);
            if (!IsCreated || MainPath.StrokeDashArray != null && MainPath.StrokeDashArray.Count > 0) return;
            RealThickness = MainPath.StrokeThickness;
            _thicknessChanged = true;
            MainPath.StrokeThickness = MainPath.StrokeThickness + 2;
        }

        protected override void OnMouseLeave(MouseEventArgs e)
        {
            base.OnMouseLeave(e);
            if (!IsCreated) return;
            if (_thicknessChanged)
                NormalizeThickness();
        }

        private void NormalizeThickness()
        {
            MainPath.StrokeThickness = RealThickness;
            _thicknessChanged = false;
        }

        private void Rectangle_PreviewMouseUp(object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton == MouseButton.Right && e.ButtonState == MouseButtonState.Released)
                MainPath.ContextMenu.IsOpen = true;
            e.Handled = true;
        }

        private void MainPath_PreviewMouseUp(object sender, MouseButtonEventArgs e)
        {
            //OnSelected(sender, new SelectionEventArgs(this));
            e.Handled = true;
        }

        private void MainPath_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (!IsCreated) return;

            this.Focus();
            MainPath.ContextMenu.IsOpen = true;
            e.Handled = true;
        }

        private void MainPath_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
        {
            e.Handled = true;
        }

        private void MainPath_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            OnCompleteBinding(sender, new CompleteBindingEventArgs(this));
            OnSelected(sender, new SelectionEventArgs(this));

            e.Handled = true;
        }

        #region Dragging

        public bool IsBeingDragged
        {
            get { return (bool)GetValue(IsBeingDraggedProperty); }
            set
            {
                SetValue(IsBeingDraggedProperty, value);
                if (value)
                {
                    foreach (var btn in ButtonPanel.Children.OfType<Button>())
                        btn.Opacity = 0.1;
                    MainPath.Opacity = 0.5;
                }
                else
                {
                    foreach (var btn in ButtonPanel.Children.OfType<Button>())
                        btn.Opacity = 1;
                    MainPath.Opacity = 1;
                }
            }
        }

        // Using a DependencyProperty as the backing store for IsBeingDragged.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsBeingDraggedProperty =
            DependencyProperty.Register("IsBeingDragged", typeof(bool), typeof(NodeFrame), new UIPropertyMetadata(false));


        private void DragButton_MouseDown(object sender, MouseButtonEventArgs e)
        {
            OnDragInit(sender, new DragInitEventArgs(this, e.GetPosition((UIElement)Parent)));
            e.Handled = true;
        }


        #endregion

        #region Validation
        private bool ValidateDoubleCollection(string arg)
        {
            return ValidatableTextBox.ValidateDoubleCollection(arg);
        }

        private bool ValidateDouble(string arg)
        {
            return ValidatableTextBox.ValidateDouble(arg);
        }

        private bool ValidateThickness(string arg)
        {
            if (!ValidatableTextBox.ValidateDouble(arg)) return false;
            double value;
            if (!double.TryParse(arg.Replace(",", "."), NumberStyles.Number, CultureInfo.InvariantCulture, out value))
                return false;

            return value <= ApplicationSettings.Default.MaxBinderThickness
                   && value >= ApplicationSettings.Default.MinBinderThickness;
        }

        private bool ParseDoubleCollection(string arg)
        {
            return ValidatableTextBox.ParseDoubleCollection(arg);
        }
        #endregion

        private void MainPath_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            TopCenter = TopCenter;
            OnTopCenterChanged();
        }

        private bool ValidateFrameSize(string arg)
        {
            if (!ValidatableTextBox.ParseDouble(arg)) return false;

            double value = double.Parse(arg.Replace(",", "."));
            if (value < ApplicationSettings.Default.MinimalFrameSize) return false;
            return true;
        }

        private void Menu_Closed(object sender, RoutedEventArgs e)
        {
            double thickness = double.Parse(ThicknessBox.Text.Replace(",", "."), CultureInfo.InvariantCulture);
            DesiredThickness = thickness;
            //normalizes thickness & invokes redraw
            OnMenuClosed(sender, new MenuChangesEventArgs(this));
        }

        private void Menu_Opened(object sender, RoutedEventArgs e)
        {
            ThicknessBox.Text = DesiredThickness.ToString();
            OnMenuOpened(sender, new MenuChangesEventArgs(this));
        }

        private void HandleEvent(object sender, MouseButtonEventArgs e)
        {
            e.Handled = true;
        }

        public Dictionary<string, object> MakeSnapshot()
        {
            var snapshot = new Dictionary<string, object>();
            snapshot.Add("Size", Size);
            snapshot.Add("Thickness", DesiredThickness);
            snapshot.Add("Dashes", MainPath.StrokeDashArray);
            snapshot.Add("Brush", MainPath.Stroke);

            return snapshot;
        }

        public void RestoreSnapshop(Dictionary<string, object> snapshot)
        {
            if (snapshot.ContainsKey("Size") && Size != (Size) snapshot["Size"])
                Size = (Size)snapshot["Size"];
            if (snapshot.ContainsKey("Thickness") && DesiredThickness != (double) snapshot["Thickness"])
                DesiredThickness = (double)snapshot["Thickness"];
            if (snapshot.ContainsKey("Dashes") && MainPath.StrokeDashArray != snapshot["Dashes"])
                MainPath.StrokeDashArray = (DoubleCollection) snapshot["Dashes"];
            if (snapshot.ContainsKey("Brush") && MainPath.Stroke != snapshot["Brush"])
                MainPath.Stroke = (Brush) snapshot["Brush"];
        }

        public bool DetectChanges(Dictionary<string, object> snapshot)
        {
            if (snapshot.ContainsKey("Size") && Size != (Size) snapshot["Size"])
                return true;
            if (snapshot.ContainsKey("Thickness") && DesiredThickness != (double)snapshot["Thickness"])
                return true;
            if (snapshot.ContainsKey("Dashes") && MainPath.StrokeDashArray != snapshot["Dashes"])
                return true;
            if (snapshot.ContainsKey("Brush") && MainPath.Stroke != snapshot["Brush"])
                return true;
            return false;
        }

        public event Action<object, MenuChangesEventArgs> MenuOpened;

        protected virtual void OnMenuOpened(object sender, MenuChangesEventArgs e)
        {
            Action<object, MenuChangesEventArgs> handler = MenuOpened;
            if (handler != null) handler(sender, e);
        }

        public event Action<object, MenuChangesEventArgs> MenuClosed;

        protected virtual void OnMenuClosed(object sender, MenuChangesEventArgs e)
        {
            Action<object, MenuChangesEventArgs> handler = MenuClosed;
            if (handler != null) handler(sender, e);
        }
    }
}
